# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import datetime
import functools
import logging
import sys

from coin.base.datetime_util import to_datetime
from coin.base.timestamp import get_timestamp

from coin.exchange.bitflyer_v1.kr_rest.futures_product import BitflyerFuturesProduct
from coin.exchange.bitmex.kr_rest.futures_product import BitmexFuturesProduct
from coin.exchange.okex_futures.kr_rest.futures_product import (OkexCurrency, OkexFuturesProduct)

from coin.exchange.okex_futures.types import OkexFuturesOrderSide
from coin.strategy.mm.base.dump.pass_unhedge import FillRow, FeatureRow
from coin.strategy.mm.base.bookmap import BookMap
from coin.strategy.mm.base.pass_unhedge import Pricer

from coin.strategy.mm.okex_futures.simple_mm_executor_2 import OkexFuturesSimpleMMExecutor_v2
from coin.strategy.mm.base.okex_futures_strategy_base import (OkexFuturesPositionBalancePrinter,
                                                              OkexFuturesStrategyBase)


class PassiveUnhedgeStrategy(OkexFuturesStrategyBase):
  def __init__(self, strategy_params, dry_run=True):
    trade_product = strategy_params['trade_product']
    super().__init__(products=[trade_product],
                     currencies=strategy_params['currencies'],
                     print_balance=False,
                     logger=logging.getLogger('PassiveUnhedgeStrategy'))

    self._strategy_params = strategy_params
    self._dry_run = dry_run

    self._trade_product = trade_product
    self._ref_product = strategy_params['ref_product']
    self._edge = strategy_params['edge_bp'] / 10000.
    self._agg_edge = None
    if strategy_params.get('agg_edge_bp', None) is not None:
      self._agg_edge = strategy_params['agg_edge_bp'] / 10000.

    self._bookmap = BookMap([self._trade_product, self._ref_product], logger=self._logger)
    self._pricer = Pricer(**strategy_params['pricing'])
    self._executor = None

    self._printer = OkexFuturesPositionBalancePrinter(OkexCurrency.FromStrCurrency('BTC'),
                                                      self._trade_product,
                                                      self._bookmap,
                                                      self._logger)
    self._last_feature_dump_ts = 0

    # Print header
    self._logger.info(FillRow.get_header())
    self._logger.info(FeatureRow.get_header())

  def prepare(self):
    assert self.feed_subsystem is not None

    # Subscribe feed.
    for product in (self._trade_product, self._ref_product):
      self.feed_subsystem.subscribe_book(product, functools.partial(self.on_book, product))

    # Start balance and position printing.
    self._printer.start_print(self._ioloop, 30)

  def on_og_reset(self):
    self._printer.register_order_gateway(self.order_gateway)

    # Intialize passive executor
    self._logger.info('Intializing executor...')
    self._executor = OkexFuturesSimpleMMExecutor_v2(self._strategy_params['executor'],
                                                    self.order_gateway,
                                                    self._logger)
    self.order_gateway.register_fill_callback(self._on_fill)

  def on_book(self, product, book):
    self._bookmap.set_book(product, book)

    # Check book freshness and sanity
    if not self._bookmap.is_book_ready() or not self._bookmap.is_book_sane():
      return

    self._pricer.update(self._bookmap.get_book(self._ref_product),
                        self._bookmap.get_book(self._trade_product))

    if not self._pricer.ready or not self.is_og_ready() or self._dry_run:
      return

    askp, bidp = self._pricer.calculate_price(self._edge, self._edge, True)

    aggression = False
    if self._agg_edge is not None:
      agg_askp, agg_bidp = self._pricer.calculate_price(self._agg_edge, self._agg_edge, False)

      if agg_askp < askp:
        askp = agg_askp
        aggression = True

      if agg_bidp > bidp:
        bidp = agg_bidp
        aggression = True

    fired = self._executor.manage_mm_orders(askp, bidp)
    if fired and aggression:
      if agg_askp == askp:
        self._logger.info('AGG SELL %.2f', agg_askp)
      elif agg_bidp == bidp:
        self._logger.info('AGG BUY  %.2f', agg_bidp)

    if book.timestamp - self._last_feature_dump_ts >= 2 * (10**9):
      self._last_feature_dump_ts = book.timestamp
      self._logger.info(self._pricer.last_value.to_log_string())

  def _on_fill(self, fill_price, fill_qty, order):
    self._dump_fill(fill_price, fill_qty, order.order_side, order.internal.fee)

  def _dump_fill(self, fill_price, fill_qty, order_side, fee):
    timestamp = get_timestamp()

    pos, unrealized_pnl = self._printer._get_pos(self._trade_product)
    balance = self._og.get_balance(self._trade_product.base)
    balance += unrealized_pnl

    if order_side in (OkexFuturesOrderSide.BUY_OPEN, OkexFuturesOrderSide.BUY_CLOSE):
      side_sign = 1
    elif order_side in (OkexFuturesOrderSide.SELL_OPEN, OkexFuturesOrderSide.SELL_CLOSE):
      side_sign = -1
    else:
      raise ValueError('Invalid order side: %s' % order_side)

    pos += side_sign * fill_qty

    row = FillRow(time=timestamp,
                  time_hr=to_datetime(timestamp),
                  exchange='OkexFutures',
                  product=str(self._trade_product),
                  side=side_sign,
                  price=fill_price,
                  qty=fill_qty,
                  maker=-1,
                  fee=fee,
                  pos=pos,
                  balance=balance)
    self._logger.info(row.to_log_string())


def get_ref_product(exchange):
  if exchange == 'Bitmex':
    ref_product = BitmexFuturesProduct.FromStr('BTC-USD.PERPETUAL')
    ref_product_true_book_funcs = ((lambda book: book.get_notional_asks_by_qty(10000.)[1][-1][0]),
                                   (lambda book: book.get_notional_bids_by_qty(10000.)[1][-1][0]))
  elif exchange == 'Bitflyer':
    ref_product = BitflyerFuturesProduct.FromStr('BTC-JPY.PERPETUAL')
    ref_product_true_book_funcs = ((lambda book: book.get_notional_asks_by_qty(1.)[1][-1][0]),
                                   (lambda book: book.get_notional_bids_by_qty(1.)[1][-1][0]))
  else:
    raise ValueError(exchange)

  return ref_product, ref_product_true_book_funcs


def get_param_set(target, og_config=None, init_og_immediately=False):
  trade_product = OkexFuturesProduct.FromStr('BTC-USD.QUARTER')
  trade_product_true_book_funcs = ((lambda book: book.get_notional_asks_by_qty(100.)[1][-1][0]),
                                   (lambda book: book.get_notional_bids_by_qty(100.)[1][-1][0]))

  agg_edge = None

  if target == 'bitmex-02m07':
    okex_futures_og_config = 'okex_xunke06'
    ref_product, ref_product_true_book_funcs = get_ref_product('Bitmex')
    price_multiplier_1 = 1.
    basis_ma_window = 2.
    edge = 7.  # bp
    lot_size = 10
    stack = 5

  elif target == 'bitmex-05m07':
    okex_futures_og_config = 'okex_xunke02'
    ref_product, ref_product_true_book_funcs = get_ref_product('Bitmex')
    price_multiplier_1 = 1.
    basis_ma_window = 5.
    edge = 7.  # bp
    lot_size = 10
    stack = 5

  elif target == 'bitmex-10m07':
    okex_futures_og_config = 'okex_xunke07'
    ref_product, ref_product_true_book_funcs = get_ref_product('Bitmex')
    price_multiplier_1 = 1.
    basis_ma_window = 10.
    edge = 7.  # bp
    lot_size = 10
    stack = 5

  else:
    raise ValueError('Invalid target: %s', target)
    '''
  elif target == 'quarter-05m07':
    okex_futures_og_config = 'okex_xunke05'
    basis_ma_window = 5.
    edge = 7.  # bp
    agg_edge = 11
    lot_size = 10
    stack = 5

  elif target == 'quarter-05m09':
    okex_futures_og_config = 'okex_jshin1'
    basis_ma_window = 5.
    edge = 9.  # bp
    agg_edge = 13
    lot_size = 10
    stack = 5

  elif target == 'quarter-08m07':
    okex_futures_og_config = 'okex_xunke06'
    basis_ma_window = 8.
    edge = 7.  # bp
    agg_edge = 11
    lot_size = 10
    stack = 5

  elif target == 'quarter-08m09':
    okex_futures_og_config = 'okex_xunke07'
    basis_ma_window = 8.
    edge = 9.  # bp
    agg_edge = 13
    lot_size = 10
    stack = 5

  elif target == 'test':
    okex_futures_og_config = 'okex_xunke02'
    basis_ma_window = 5.
    edge = 6.  # bp
    agg_edge = None
    lot_size = 1
    stack = 3
  '''

  if og_config is not None:
    logging.info('Overriding OG config: %s (from %s).', og_config, okex_futures_og_config)
    okex_futures_og_config = og_config

  min_pos = -lot_size * stack
  max_pos = lot_size * stack
  NS_PER_MINUTE = 60 * (10**9)
  strategy_params = {
      'currencies': [OkexCurrency.FromStrCurrency('BTC')],
      'trade_product': trade_product,
      'ref_product': ref_product,
      'edge_bp': edge,
      'agg_edge_bp': agg_edge,
      'pricing': {
          'basis_ma_window': basis_ma_window * NS_PER_MINUTE,
          'tick': 0.01,
          'book_askt_func_1': ref_product_true_book_funcs[0],
          'book_bidt_func_1': ref_product_true_book_funcs[1],
          'book_askt_func_2': trade_product_true_book_funcs[0],
          'book_bidt_func_2': trade_product_true_book_funcs[1],
          'price_multiplier_1': price_multiplier_1
      },
      'executor': {
          'product': trade_product,
          'lot_size': lot_size,
          'stickiness': 0.,
          'min_pos': min_pos,
          'max_pos': max_pos,
          'order_update_period': 1.,
          'do_not_submit_after_cancel': True
      }
  }

  # Trading environment config
  if init_og_immediately:
    init_og_after_min = 0
  else:
    init_og_after_min = basis_ma_window + 0.1

  exit_after_min = init_og_after_min + 30
  env_config = {
      'og_config_name': okex_futures_og_config,
      'init_order_gateway_after': datetime.timedelta(minutes=init_og_after_min),
      'exit_after': datetime.timedelta(minutes=exit_after_min)
  }

  return strategy_params, env_config


def main(argv):
  assert len(argv) in [2, 3]
  target = argv[1]
  og_config = None
  if len(argv) == 3:
    og_config = argv[2]
  strategy_params, env_config = get_param_set(target, og_config, init_og_immediately=False)

  # Run strategy!
  strategy = PassiveUnhedgeStrategy(strategy_params, dry_run=False)
  return PassiveUnhedgeStrategy.run_live(
      strategy=strategy,
      og_config_name=env_config['og_config_name'],
      init_order_gateway_after=env_config['init_order_gateway_after'],
      exit_after=env_config['exit_after'],
      on_order_gateway_reset_callback=strategy.on_og_reset)


if __name__ == '__main__':
  logging.basicConfig(level='DEBUG', format='%(levelname)8s %(asctime)s %(name)s] %(message)s')
  sys.exit(main(sys.argv))
